查看原文
其他

如何巧用设计图提高 UI 的还原度?

乐府-小学生 COCOS 2022-06-10

本篇文章作者:乐府-小学生

乐府-小学生:乐府前端核心开发,从事游戏开发多年,动手能力强。从 Cocos2d-x 1.x 用到 3.x,擅长研究 Cocos 引擎原理和相关工具开发。对 Cocos Creator 的易用性和便捷性爱不释手,希望能与更多技术达人深入交流,同时为 Cocos 社区做更多贡献。更多作品《如何在 Cocos Creator 中优雅地嵌套 Prefab》


前言

UI 界面是游戏的重要组成部分,也是玩家在游戏内接触最多的东西。一个 UI 界面的感官体验直接决定着这个游戏的生命。

那么一个好的 UI 界面,不仅要美术同学设计的好,还要前端/UI(一些公司会有专门拼 UI 的)还原的好。

如果一个界面设计属于 S,而放入游戏却只有 C,那岂不是相当可惜。

所以我这篇文章将介绍,如何在 Cocos Creator 中更方便快捷地使用设计图,提高 UI 还原度。

我调研过一些公司如何去提高 UI 还原度问题,大概分为一下几种:

第一类:就是美术标好坐标边距,然后前端根据设计图的标注拼界面;

缺点:美术费事费力,前端亦然,切还原度还不高。

第二类:把设计图图放到界面后,对着图放控件,然后拼完移除;

缺点:美术还原度高,但是用完需要手动移除,有忘记移除的风险。

第三类:使用工具生成对应的界面,然后做微调。

缺点:对美术要求高,需要按照某个标准去制作,导出来的 Prefab 参差不齐。

总结以上问题,我采用第二种方案,然后在此基础上开发特定脚本来避免忘记移除的问题(功能的灵感来自 FairyGUI)

一、功能设计

1、需求分析

首先我们需要的基本功能大概就是,可以把设计图放到界面里,无额外的节点产生,并且不能影响界面中的操作。所以总结需求结合后期使用,总结出来设计图应该具备以下特征:

  1. 不影响界面控件的点击

  2. 设计图的节点在层级树里不能显示,但仍然可以设置位置和透明度等

  3. 可设置显示的层级,在界面最上端还是在最低端

  4. 不会被 Creator 计算引用,并打包到发布的项目中

  5. 仅在当前界面显示,复制或嵌套等不能显示

功能解释:

1.不影响界面控件的点击

我们添加的设计图仅用来对位的,如果能够选中会影响界面布局等。

2.设计图的节点在层级树里不能显示,但仍然可以设置位置和透明度等

主要是设计图不是界面的一部分,只是一个辅助工具,所以不应该显示再层级树里,但是为了方便对位,又需要设置透明度和位置偏移等。

3.可设置显示的层级,在界面最上端还是在最低端

为了可以方便用户选中设计图在顶端还是底端,防止设计图影响界面效果显示。

4.不会被 Creator 计算引用,并打包到发布的项目中

这个是最重要的部分,也是不好实现的部分。如果界面中直接引用了某个图片,在发布项目的时候,Creator 会自动计算依赖关系并打包到项目中,而设计图只是用来拼界面的。不需要,也不能被打包到项目中。

5.仅在当前界面显示,复制或嵌套等不能显示

如果当前 prefab 被其他界面引用,或者是嵌套使用,当前界面的预览图不能显示。因为当前界面的设计图往往是当前界面的样式,所以仅需要在当前界面显示,在其他界面显示反而会影响其他界面的开发。

2、实现方案

首先我们先把设计图能显示到界面中:

const {ccclass, property, executeInEditMode} = cc._decorator;
@ccclass@executeInEditMode // 在编辑器中执行export default class DesignView extends cc.Component {
@property private _spriteFrame: cc.SpriteFrame = null;
@property({type: cc.SpriteFrame, visible: true, displayName: "设计图"}) public set spriteFrame(frame: cc.SpriteFrame) { this._updateDesignView(frame); }
public get spriteFrame(): cc.SpriteFrame { return this._spriteFrame; }
private _sprite: cc.Sprite = null;
private _updateDesignView(frame: cc.SpriteFrame, init: boolean = false) { if (this._spriteFrame === frame && !init) { return; } if (!this._sprite) { let viewNode = new cc.Node("DesignView"); this._sprite = viewNode.addComponent(cc.Sprite); viewNode.parent = this.node; } this._sprite.spriteFrame = frame; this._spriteFrame = frame; }
public onLoad() { // 激活当前界面的时候显示预览图 this._updateDesignView(this._spriteFrame, true); } // 当前脚本激活时,显示预览图 public onEnable() { if (this._sprite) { this._sprite.enabled = true; } }
// 当前脚本取消激活时,隐藏预览图 public onDisable() { if (this._sprite) { this._sprite.enabled = false; } }
public onDestroy() { // 当前脚本被手动移除时,删除预览图 if (this._sprite && !(this._sprite.node["_objFlags"] & cc.Object["Flags"].Destroying)) { this._sprite.node.destroy(); } }}

简单的功能实现了,把设计图拖到后边的设计图属性上,界面出来了。
但是这只是设计图,我们不想让他在左上角的层级树显示,因为并且也不能都点击到,这样才能方便我们布局界面。如果有看我写过嵌套的小伙伴可能就比较清楚了,这里我就不做过多的赘述了:
viewNode["_objFlags"] |= (cc.Object["Flags"].DontSave | cc.Object["Flags"].LockedInEditor | cc.Object["Flags"].HideInHierarchy);
完成上面的步骤之后,我们再来看看界面中①②③④,是不是功能已经实现了,把预览图拖到对应的位置,界面中显示,并且不会多出其他任何信息。

到这我们已经完成 50% 了,但是还有两个比较重要的问题还没显示。其一,就是发布项目的时候,设计图不能被打包到项目中,这才是我们要实现的精髓部分。这个功能实现不了,整个功能就是白费。

通过各种查阅古籍和翻阅官方文档,我终于知道了一个救命稻草 editorOnly,你们现在是不是也和我一样揣着无比喜悦的心情。最困难的问题终于要得到解决了。

@property({editorOnly: true})private _spriteFrame: cc.SpriteFrame = null
修改好了以后,发布项目测试。

What?还在,说好的 在导出项目前剔除该属性呢?(Cocos:此问题已在 v2.4.3 修复)好吧!没办法了只能另辟蹊径了。

这时候只能想看看本地是如何识别纹理的。

打开对应的 prefab,找到对应的位置格式就是"__uuid__":"xxx",所以这里我大胆的猜测官方就是识别的__uuid__,而和后面的值一点关系都没有,所以如果我保存一个其他类型值他是不是就不知道是否引用了呢?

@ccclass@executeInEditModeexport default class DesignView extends cc.Component {
// 删除@property private _spriteFrame: cc.SpriteFrame = null;
// 添加保存纹理的属性 @property private _designUrl: string = "";.... private _updateDesignView(frame: cc.SpriteFrame, init: boolean = false) { if (this._spriteFrame === frame && !init) { return; } this._designUrl = frame ? frame["_uuid"] : ""; if (!this._sprite) { let viewNode = new cc.Node("DesignView"); this._sprite = viewNode.addComponent(cc.Sprite); viewNode.parent = this.node; viewNode["_objFlags"] |= (cc.Object["Flags"].DontSave | cc.Object["Flags"].LockedInEditor | cc.Object["Flags"].HideInHierarchy); } this._sprite.spriteFrame = frame; this._spriteFrame = frame; }...
现在再执行导出操作,确实项目中不会有设计图资源了。但是又出现了另一个问题,那就是我们重新打开当前界面的时候,设计图也不会被创建了。因为_spriteFrame已经不是编辑器属性了,所以他不会被默认创建了,我们现在有的就是纹理的 uuid,所以我们现在能做的就是手动创建纹理出来。

public onLoad() { if (this._designUrl) { if (cc.assetManager && cc.assetManager.loadAny) { cc.assetManager.loadAny({type: "uuid", uuid: this._designUrl}, null, (err, spriteFrame: cc.SpriteFrame)=> { if (!err) { this._updateDesignView(spriteFrame); } }) } else { // 兼容2.4.0之前版本 cc.loader.load({type: "uuid", uuid: this._designUrl}, null, (err, spriteFrame: cc.SpriteFrame)=> { if (!err) { this._updateDesignView(spriteFrame); } }) } } }
修改完以后,测试成功,喜大普奔。这里我就贴效果图了。到这里我们的主要功能就实现完了。下面就是如何实现被嵌套使用的时候不显示设计图问题了。(这里我就不过多解释原因了。)

如何实现设计图只在当前界面生效呢?

方案一:判断当前界面是不是在 Scene 层显示(我们所打开的所有界面,场景或者预制体的 parent 都是一个 scene)。

方案二:找到一个唯一性标识的字段。

对于场景(Scene)来说,不会出现场景嵌套场景的情况,所以这里不做讨论。

通过代码和 prefab 文件的研究,发现有个 fileId 属性,当 prefab 的 parent 不是 scene 时,这个 id会 变成,prefab 的 parent 的 fileId。这里我们刚好可以利用这一点,在第一次挂在 DesignView 脚本时,保存 fileId 来记录默认的界面,一旦 id 发生改变,设计图就不显示。

private _isCurPrefab(): boolean { let prefab = this.node["_prefab"]; return prefab && prefab.root && (!this._prefabFileId || this._prefabFileId === prefab.fileId);}
到这里,我们的核心功能已经实现了。但是因为设计图的节点被我们隐藏了,所以我们要添加一些接口可以用来这是透明度、层级和位置偏移等。下面是实现的最终版代码。

const {ccclass, property, executeInEditMode} = cc._decorator;
@ccclass@executeInEditModeexport default class DesignView extends cc.Component {
private _sprite: cc.Sprite = null; private _spriteFrame: cc.SpriteFrame = null;
@property private _prefabFileId: string = "";
@property private _opacity: number = 70;
@property private _offsetX: number = 0;
@property private _offsetY: number = 0;
@property private _showTop: boolean = true;
@property private _designUrl: string = "";
@property({type: cc.SpriteFrame, displayName: "设计图"}) public set spriteFrame(frame: cc.SpriteFrame) { this._updateDesignView(frame); }
public get spriteFrame(): cc.SpriteFrame { return this._spriteFrame; }
@property({type: cc.Boolean, displayName: "置顶"}) public set showTop(top: boolean) { this._showTop = top; this._updateOrder(); }
public get showTop(): boolean { return this._showTop; }
@property({type: cc.Integer, displayName: "透明度", slide: true, min : 0, max: 255}) public set opacity(value: number) { this._opacity = value; this._updateOpatity(); }
public get opacity(): number { return this._opacity; }
@property({type: cc.Integer, displayName: "X偏移"}) public set offsetX(value: number) { this._offsetX = value; this._updateOffset(); }
public get offsetX(): number { return this._offsetX; }
@property({type: cc.Integer, displayName: "Y偏移"}) public set offsetY(value: number) { this._offsetY = value; this._updateOffset(); }
public get offsetY(): number { return this._offsetY; }
private _updateDesignView(frame: cc.SpriteFrame) { if (this._spriteFrame === frame) { return; } this._designUrl = frame ? frame["_uuid"] : ""; if (!this._sprite) { let viewNode = new cc.Node("DesignView"); this._sprite = viewNode.addComponent(cc.Sprite); viewNode.parent = this.node; viewNode["_objFlags"] |= (cc.Object["Flags"].DontSave | cc.Object["Flags"].LockedInEditor | cc.Object["Flags"].HideInHierarchy);
this._updateOffset(); this._updateOrder(); this._updateOpatity(); } this._sprite.spriteFrame = frame; this._spriteFrame = frame; }
private _updateOffset() { if (!this._sprite) { return; } let node = this._sprite.node; node.x = this._offsetX; node.y = this._offsetY; }
private _updateOrder() { if (!this._sprite) { return; } this._sprite.node.zIndex = this._showTop ? 999 : -1; }
private _updateOpatity() { if (!this._sprite) { return; } this._sprite.node.opacity = this._opacity; }
public onLoad() { if (!CC_EDITOR) { return; } if (!this._prefabFileId && this.node["_prefab"]) { this._prefabFileId = this.node["_prefab"].fileId; } if (this._isCurPrefab()) { if (this._designUrl) { if (cc.assetManager && cc.assetManager.loadAny) { cc.assetManager.loadAny({type: "uuid", uuid: this._designUrl}, null, (err, spriteFrame: cc.SpriteFrame)=> { if (!err) { this._updateDesignView(spriteFrame); } }) } else { // 兼容2.4.0之前版本 cc.loader.load({type: "uuid", uuid: this._designUrl}, null, (err, spriteFrame: cc.SpriteFrame)=> { if (!err) { this._updateDesignView(spriteFrame); } }) } } if (this._sprite) { this._updateOffset(); this._updateOrder(); this._updateOpatity(); } } else { this.destroy(); } }
private _isCurPrefab(): boolean { let prefab = this.node["_prefab"]; return prefab && prefab.root && (!this._prefabFileId || this._prefabFileId === prefab.fileId); }
public onEnable() { if (this._sprite) { this._sprite.enabled = true; } }
public onDisable() { if (this._sprite) { this._sprite.enabled = false; } }
public onDestroy() { if (this._sprite && !(this._sprite.node["_objFlags"] & cc.Object["Flags"].Destroying)) { this._sprite.node.destroy(); } }}
最终界面显示效果如下:

现在我们在开发过程中,只要把 design-view 挂载到跟节点,然后把设计图拖到设计图属性位置,就可以愉快的开发了,也不用担心设计图资源忘记移除,被打包到项目中了 (除非你把设计图放到 resources 目录下,你这样做了,我也帮不了你)




以上是由 Cocos 开发者 乐府-小学生 分享的优质技术教程,此文同时参加了 Cocos 中文社区征稿活动,入选优秀稿件。欢迎各位开发者点击【阅读原文】查看原文,为作者点赞,与作者进行交流学习!


如果您在使用 Cocos 引擎的过程中,获得了独到的开发心得、见解或是方法,并且乐于分享出来,帮助更多开发者解决技术问题,加速游戏开发效率,期待您与我们联系!


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存